// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: xguo

#pragma once

#include <cstdio>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

#include <fmt/format.h>

#include "coin2/base/config/app_config.h"
#include "coin2/base/log.h"
#include "coin2/exchange/base/market/market_exchange_api.h"
#include "coin2/exchange/base/symbology/currency.h"
#include "coin2/exchange/base/symbology/symbol.h"

namespace coin2::exchange::base::symbology {

/**

========== Futures.Huobi.v1 ==========
                   product: BTC-USD.20200327
            native_product: BTC200327
       subscription_symbol: BTC-USD.QUARTER
native_subscription_symbol: BTC_CQ
       base  currency: BTC
 base native_currency: btc
 base    rep_currency: BTC
       quote currency: USD
quote native_currency: usd
quote    rep_currency: USD

========== Futures.Okex.v3 ==========
                   product: BTC-USD.20200327
            native_product: BTC-USD-200327
       subscription_symbol: BTC-USD.QUARTER
native_subscription_symbol: BTC-USD-200327
       base  currency: BTC
 base native_currency: BTC
 base    rep_currency: BTC
       quote currency: USD
quote native_currency: USD
quote    rep_currency: USD

**/

class IProduct : public ISymbol {
 public:
  using TKey = typename std::tuple<MarketType, ExchangeType, std::string>;
  virtual MarketType market() const = 0;
  virtual ExchangeType exchange() const = 0;
  virtual const std::string& market_name() const = 0;
  virtual const std::string& exchange_name() const = 0;
  virtual const TKey& key() const = 0;
  virtual const std::string& key_string() const = 0;

// /// hash can conflict. do not use as uid
// /// Market.Exchange.BASE-QUOTE.date
// /// MarketType < 10 = 4 bits
// /// ExchangeType < 500 = 9 bits
// /// BASE <= 5 bytes
// /// QUOTE<= 5 bytes
// /// date <= 915615 (2200y) <= 20 bits     416 y + 32 m + d
// const uint64_t hash() const { return 0; }

// // uuid may change across run instances
// const uint64_t uuid() const { return 0; }

// ** string representations ** //
/* ----------------------- recommended symbol methods ----------------------- */

// symbol used for subscription (feed msg may have different form)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

  virtual const std::string& feed_native_symbol() const { return native_subscription_symbol(); }
  virtual const std::string& feed_native_symbol(const std::string& api_version) const {
    return native_subscription_symbol(api_version);
  }

  // symbol used to make order (received order msg may have different form)
  virtual const std::string& order_native_symbol() const { return native_product(); }
  virtual const std::string& order_native_symbol(const std::string& api_version) const {
    return native_product(api_version);
  }

#pragma GCC diagnostic pop

  // recommended
  virtual const std::string& relative_norm() const = 0;  // BCHABC-USD.WEEK
  virtual const std::string& absolute_norm() const = 0;  // BCHABC-USD.20191206

  /* ---------------------------- deprecated symbol methods ---------------------------- */
  [[deprecated("Use absolute_norm() instead")]] virtual const std::string& product()
      const = 0;  // BCHABC-USD.20191206
  [[deprecated("Use order_native_symbol() instead")]] virtual const std::string& native_product()
      const = 0;  // BCH_CW
  [[deprecated("Use order_native_symbol(api_version) instead")]] virtual const std::string&
  native_product(const std::string& version) const = 0;
  [[deprecated("Use relative_norm() instead")]] virtual const std::string& subscription_symbol()
      const = 0;  // BCHABC-USD.WEEK
  [[deprecated("Use feed_native_symbol() instead")]] virtual const std::string&
  native_subscription_symbol() const = 0;  // BCH_CW
  [[deprecated("Use feed_native_symbol(api_version) instead")]] virtual const std::string&
  native_subscription_symbol(const std::string& version) const = 0;

  virtual const ICurrency& base() const = 0;
  virtual const ICurrency& quote() const = 0;
  virtual int64_t resolve_timestamp() const = 0;
  virtual IProduct* Clone() const = 0;
  virtual ~IProduct() = default;
};

// Use this class for non currency-pairs,
// e.g. KRX01234 until we have proper classes for them
class PseudoProduct : public IProduct {
 public:
  explicit PseudoProduct(const std::string& symbol)
      : symbol_(symbol),
        base_("[base]"),
        quote_("[quote]"),
        key_{{}, {}, symbol_},
        key_string_{fmt::format("{}:{}:{}", MARKET, EXCHANGE, symbol_)} {}

  const std::string& symbol() const override { return symbol_; }
  const std::string& native_symbol() const override { return symbol_; }

  MarketType market() const override { return {}; }
  ExchangeType exchange() const override { return {}; }
  const std::string& market_name() const override { return MARKET; }
  const std::string& exchange_name() const override { return EXCHANGE; }

  const TKey& key() const override { return key_; }
  const std::string& key_string() const override { return key_string_; }

  const std::string& feed_native_symbol() const override { return symbol_; }
  const std::string& feed_native_symbol(const std::string& api_version) const override {
    return symbol_;
  }
  const std::string& order_native_symbol() const override { return symbol_; }
  const std::string& order_native_symbol(const std::string& api_version) const override {
    return symbol_;
  }
  const std::string& relative_norm() const override { return symbol_; }
  const std::string& absolute_norm() const override { return symbol_; }

  const std::string& product() const override { return symbol_; }
  const std::string& native_product() const override { return symbol_; }
  const std::string& native_product(const std::string& version) const override { return symbol_; }
  const std::string& subscription_symbol() const override { return symbol_; }
  const std::string& native_subscription_symbol() const override { return symbol_; }
  const std::string& native_subscription_symbol(const std::string& version) const override {
    return symbol_;
  }
  int64_t resolve_timestamp() const { return resolve_timestamp_; }

  /* ---------------------------- deprecated symbol methods ---------------------------- */
  const ICurrency& base() const override { return base_; }
  const ICurrency& quote() const override { return quote_; }
  IProduct* Clone() const override { return new PseudoProduct(symbol_); }

  const std::string MARKET = "virtual_market";
  const std::string EXCHANGE = "virtual_exchange";
  std::string symbol_;
  PseudoCurrency base_;
  PseudoCurrency quote_;
  int64_t resolve_timestamp_{0};
  const TKey key_;
  const std::string key_string_;
};

inline bool operator==(const IProduct& lhs, const IProduct& rhs) {
  return lhs.market() == rhs.market() && lhs.exchange() == rhs.exchange() &&
         lhs.absolute_norm() == rhs.absolute_norm();
}

template <typename CurrencyT>
class CurrencyPairProduct : public IProduct {
 public:
  using currency_type = CurrencyT;

  CurrencyPairProduct(
      const CurrencyT& base,
      const CurrencyT& quote,
      const std::string& product,
      const std::string& order_native_symbol,
      const std::string& subscription_symbol,
      const std::string& feed_native_symbol,
      const MarketType market_type,
      const ExchangeType exchange)
      : base_(base),
        quote_(quote),
        product_(product),
        order_symbol_(order_native_symbol),
        subscription_symbol_(subscription_symbol),
        feed_symbol_(feed_native_symbol),
        market_(market_type),
        exchange_(exchange),
        key_{market_, exchange_, product_},
        key_string_{fmt::format(
            "{}:{}:{}",
            market::MarketTypeToString(market_),
            market::ExchangeTypeToString(exchange_),
            product_)} {}

  const std::string& symbol() const final { return product_; }
  const std::string& native_symbol() const final { return order_symbol_; }

  MarketType market() const final { return market_; }

  const std::string& market_name() const final { return market::MarketTypeToString(market_); }

  ExchangeType exchange() const final { return exchange_; }

  const std::string& exchange_name() const final { return market::ExchangeTypeToString(exchange_); }

  const TKey& key() const override { return key_; }
  const std::string& key_string() const override { return key_string_; }

  const std::string& relative_norm() const override { return subscription_symbol_; }
  const std::string& absolute_norm() const override { return product_; }
  const std::string& feed_native_symbol() const override { return feed_symbol_; }
  const std::string& feed_native_symbol(const std::string& /*api_version*/) const override {
    return feed_symbol_;
  }
  const std::string& product() const final { return product_; }
  const std::string& native_product() const final { return order_symbol_; }
  // can be override
  const std::string& native_product(const std::string& /*version*/) const override {
    return order_symbol_;
  }
  const std::string& subscription_symbol() const final { return subscription_symbol_; }
  const std::string& native_subscription_symbol() const final { return feed_symbol_; }
  // can be override
  const std::string& native_subscription_symbol(const std::string& /*version*/) const override {
    return feed_symbol_;
  }

  const ICurrency& base() const final { return base_; }
  const ICurrency& quote() const final { return quote_; }
  const CurrencyT& base_impl() const { return base_; }
  const CurrencyT& quote_impl() const { return quote_; }
  int64_t resolve_timestamp() const { return resolve_timestamp_; }

 private:
  // A bit hacky, otherwise we need to change products for all exchanges.
  template <typename TProduct>
  friend class ProductFromStrMixin;
  template <typename, typename>
  friend class SpotProduct;
  template <typename, typename>
  friend class AmmProduct;
  template <typename, typename>
  friend class IndexProduct;

  void SetResolveTimestamp(int64_t resolve_timestamp) { resolve_timestamp_ = resolve_timestamp; }

 private:
  const CurrencyT base_;
  const CurrencyT quote_;
  const std::string product_;              // abs norm
  const std::string order_symbol_;         // native
  const std::string subscription_symbol_;  // rel norm
  const std::string feed_symbol_;          // native
  const MarketType market_;
  const ExchangeType exchange_;
  int64_t resolve_timestamp_;
  const TKey key_;
  const std::string key_string_;
};

template <typename TProduct>
class ProductFromStrMixin {
 public:
  // user implements
  // TProduct::FromNormStrImpl
  // TProduct::FromNativeStrImpl

  static std::unique_ptr<TProduct> FromNormStrNoThrow(
      const std::string& product_str,
      int64_t resolve_timestamp) {
    auto prod = TProduct::FromNormStrImpl(product_str, resolve_timestamp);
    if (prod) {
      prod->SetResolveTimestamp(resolve_timestamp);
    }
    return prod;
  }

  static std::unique_ptr<TProduct> FromNativeStrNoThrow(
      const std::string& product_str,
      int64_t resolve_timestamp) {
    auto prod = TProduct::FromNativeStrImpl(product_str, resolve_timestamp);
    if (prod) {
      prod->SetResolveTimestamp(resolve_timestamp);
    }
    return prod;
  }

  static TProduct FromNormStr(const std::string& product_str, int64_t resolve_timestamp) {
    std::unique_ptr<TProduct> product =
        TProduct::FromNormStrNoThrow(product_str, resolve_timestamp);
    CHECK(product) << "Unable to parse norm string " << product_str
                   << " TProduct=" << typeid(TProduct).name();
    return std::move(*product.get());
  }

  static TProduct FromNativeStr(const std::string& product_str, int64_t resolve_timestamp) {
    std::unique_ptr<TProduct> product =
        TProduct::FromNativeStrNoThrow(product_str, resolve_timestamp);
    CHECK_THROW(product) << "Unable to parse native product string: " << product_str << " as "
                         << typeid(TProduct).name();
    return std::move(*product.get());
  }

  static TProduct FromStr(const std::string& product_str, int64_t resolve_timestamp) {
    std::unique_ptr<TProduct> product;
    product = TProduct::FromNativeStrNoThrow(product_str, resolve_timestamp);
    if (product) {
      return std::move(*product);
    }
    product = TProduct::FromNormStrNoThrow(product_str, resolve_timestamp);
    if (product) {
      return std::move(*product);
    }
    CHECK(product) << "Unable to parse product string: " << product_str;
    return std::move(*product);
  }
};

class SpotProductReplaceMeLater : public CurrencyPairProduct<CurrencyReplaceMeLater> {
 public:
  SpotProductReplaceMeLater(
      const CurrencyReplaceMeLater& base,
      const CurrencyReplaceMeLater& quote,
      ExchangeType exchange,
      const SymbologySpec& spec)
      : CurrencyPairProduct<CurrencyReplaceMeLater>(
            base,
            quote,
            ConvertToSpotNormStr(base, quote),
            ConvertToNativeOrderSymbol(base, quote, spec),
            ConvertToSpotNormStr(base, quote),
            ConvertToNativeFeedSymbol(base, quote, spec),
            MarketType::Spot,
            exchange),
        exchange_(exchange),
        spec_(spec) {}

  IProduct* Clone() const final {
    return new SpotProductReplaceMeLater(
        CurrencyPairProduct<CurrencyReplaceMeLater>::base_impl(),
        CurrencyPairProduct<CurrencyReplaceMeLater>::quote_impl(),
        exchange_,
        spec_);
  }

  static std::unique_ptr<SpotProductReplaceMeLater> FromNorm(
      const std::string& norm_symbol,
      const int64_t resolve_timestamp,
      ExchangeType exchange,
      const SymbologySpec& spec,
      const ProductInfoConstant& pic) {
    std::string base_str;
    std::string quote_str;
    bool success = ParseSpotNormStr(norm_symbol, &base_str, &quote_str);
    if (success) {
      auto base =
          CurrencyReplaceMeLater::FromNorm(base_str, resolve_timestamp, exchange, spec, pic);
      auto quote =
          CurrencyReplaceMeLater::FromNorm(quote_str, resolve_timestamp, exchange, spec, pic);
      return std::make_unique<SpotProductReplaceMeLater>(base, quote, exchange, spec);
    }
    throw std::runtime_error(fmt::format("cannot create spot product from norm '{}'", norm_symbol));
  }

  static std::unique_ptr<SpotProductReplaceMeLater> FromNative(
      const std::string& native_symbol,
      const int64_t resolve_timestamp,
      ExchangeType exchange,
      const SymbologySpec& spec,
      const ProductInfoConstant& pic) {
    if (pic.HasNativeProduct(native_symbol)) {
      const coin::proto::ProductInfoProto& pi = pic.GetProductInfoByNativeSymbol(native_symbol);
      auto base = CurrencyReplaceMeLater::FromNative(
          pi.native_base(),
          resolve_timestamp,
          exchange,
          spec,
          pic);
      auto quote = CurrencyReplaceMeLater::FromNative(
          pi.native_quote(),
          resolve_timestamp,
          exchange,
          spec,
          pic);
      return std::make_unique<SpotProductReplaceMeLater>(base, quote, exchange, spec);
    }
    throw std::runtime_error(
        fmt::format("cannot create spot product from native '{}'", native_symbol));
  }

 private:
  static bool
  ParseSpotNormStr(const std::string& currency_pair, std::string* base, std::string* quote) {
    std::vector<std::string> out = Split(currency_pair, "-.");

    if (out.size() == 2 || out.size() == 3) {
      *base = out[0];
      *quote = out[1];
      return true;
    } else {
      return false;
    }
  }

  static std::string ConvertToSpotNormStr(const ICurrency& base, const ICurrency& quote) {
    return fmt::format("{}-{}", base.currency(), quote.currency());
  }

  static std::string ConvertToNativeSymbol(
      const ICurrency& base,
      const ICurrency& quote,
      const std::string& symbol_template);

  static std::string ConvertToNativeOrderSymbol(
      const ICurrency& base,
      const ICurrency& quote,
      const SymbologySpec& spec) {
    return ConvertToNativeSymbol(base, quote, spec.order_symbol);
  }

  static std::string ConvertToNativeFeedSymbol(
      const ICurrency& base,
      const ICurrency& quote,
      const SymbologySpec& spec) {
    return ConvertToNativeSymbol(base, quote, spec.feed_symbol);
  }

  MarketExchangeApi mea_;
  ExchangeType exchange_;
  SymbologySpec spec_;
};

}  // namespace coin2::exchange::base::symbology

namespace std {

template <>
struct hash<coin2::exchange::base::symbology::IProduct> {
  using IProduct = coin2::exchange::base::symbology::IProduct;
  std::size_t operator()(const IProduct& product) const {
    const auto h1 = static_cast<size_t>(product.exchange());
    const auto h2 = std::hash<std::string>()(product.symbol());
    return h1 ^ h2;
  }
};

}  // namespace std
